---
title: "Extensions"
section: "Developers"
description: "Learn how to create and develop extensions for Hyprnote"
---

# Overview

Extensions allow you to add custom functionality to Hyprnote through panels that appear in the sidebar. Each extension can define one or more panels with custom React-based UIs that integrate seamlessly with the application.

Extensions consist of two main parts: a runtime script (`main.js`) that runs in a sandboxed Deno environment, and optional UI panels (`ui.tsx`) that render React components within the Hyprnote interface.

# Extension Structure

A typical extension has the following structure:

```
my-extension/
├── extension.json    # Extension manifest
├── main.js          # Runtime script (Deno)
├── ui.tsx           # Panel UI component (React)
└── dist/
    └── ui.js        # Built panel UI (generated)
```

# Manifest

The `extension.json` manifest defines your extension's metadata and configuration:

```json
{
  "id": "my-extension",
  "name": "My Extension",
  "version": "0.1.0",
  "api_version": "0.1",
  "description": "A custom extension for Hyprnote",
  "entry": "main.js",
  "panels": [
    {
      "id": "my-extension.main",
      "title": "My Extension",
      "entry": "dist/ui.js"
    }
  ],
  "permissions": {}
}
```

The manifest fields are:

- `id`: Unique identifier for your extension (lowercase, hyphenated)
- `name`: Display name shown in the UI
- `version`: Semantic version of your extension
- `api_version`: Hyprnote extension API version (currently `0.1`)
- `description`: Brief description of what your extension does
- `entry`: Path to the runtime script
- `panels`: Array of panel definitions
- `permissions`: Required permissions (reserved for future use)

Each panel definition includes:

- `id`: Unique panel identifier (typically `extension-id.panel-name`)
- `title`: Display title shown in the panel tab
- `entry`: Path to the built UI bundle

# Runtime Script

The runtime script (`main.js`) runs in a sandboxed Deno environment and handles extension lifecycle events. The runtime provides a `hypr.log` API for logging:

```javascript
__hypr_extension.activate = function (context) {
  hypr.log.info(`Activating ${context.manifest.name} v${context.manifest.version}`);
  hypr.log.info(`Extension path: ${context.extensionPath}`);
};

__hypr_extension.deactivate = function () {
  hypr.log.info("Deactivating extension");
};

__hypr_extension.customMethod = function (arg) {
  hypr.log.info(`Called with: ${arg}`);
  return `Result: ${arg}`;
};
```

The `context` object passed to `activate` contains:

- `manifest`: The parsed extension manifest
- `extensionPath`: Absolute path to the extension directory

The runtime script is required in the manifest but currently serves primarily for logging and future extensibility. The main extension functionality comes from the panel UI.

# Panel UI

Panel UIs are React components that render within the Hyprnote interface. Create a `ui.tsx` file with a default export:

```tsx
import { useState } from "react";
import { Button } from "@hypr/ui/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@hypr/ui/components/ui/card";

export interface ExtensionViewProps {
  extensionId: string;
  state?: Record<string, unknown>;
}

export default function MyExtensionView({ extensionId }: ExtensionViewProps) {
  const [count, setCount] = useState(0);

  return (
    <div className="p-4 h-full">
      <Card>
        <CardHeader>
          <CardTitle>My Extension</CardTitle>
        </CardHeader>
        <CardContent>
          <p>Counter: {count}</p>
          <Button onClick={() => setCount((c) => c + 1)}>Increment</Button>
        </CardContent>
      </Card>
    </div>
  );
}
```

# Available Globals

Extension UIs have access to the following globals provided by Hyprnote:

## React and UI

- `react` - React library
- `react-dom` - React DOM library
- `@hypr/ui` - Hyprnote UI component library (Button, ButtonGroup, Card, Checkbox, Popover)
- `@hypr/utils` - Hyprnote utility functions (cn, date-fns helpers, etc.)

## State Management

- `tinybase/ui-react` - TinyBase React hooks for synchronized state (useRow, useStore, useSetRowCallback, etc.)

## Navigation

- `@hypr/tabs` - Tab management for opening new views

Import these as you would in a normal React application:

```tsx
import { useState, useEffect } from "react";
import { Button } from "@hypr/ui/components/ui/button";
import { cn } from "@hypr/utils";
import { useRow, useSetRowCallback, useStore } from "tinybase/ui-react";
import { useTabs } from "@hypr/tabs";
```

# State Synchronization

Extensions can read and write state that synchronizes with the main Hyprnote application using TinyBase. The synchronization happens automatically via postMessage between the iframe and parent window.

## Reading State

Use TinyBase hooks to read from the synchronized store:

```tsx
import { useRow, useStore } from "tinybase/ui-react";

function MyExtension({ extensionId }: ExtensionViewProps) {
  const store = useStore();

  // Read extension-specific state
  const extensionState = useRow("extension_state", extensionId) as {
    counter?: number;
    last_updated?: string;
  };

  // Read app data (calendars, events, sessions, etc.)
  const calendarIds = store?.getRowIds("calendars") ?? [];

  return <div>Counter: {extensionState?.counter ?? 0}</div>;
}
```

## Writing State

Use `useSetRowCallback` to write state that syncs back to the main app:

```tsx
import { useRow, useSetRowCallback } from "tinybase/ui-react";

function MyExtension({ extensionId }: ExtensionViewProps) {
  const extensionState = useRow("extension_state", extensionId) as {
    counter?: number;
    last_updated?: string;
  };

  const setExtensionState = useSetRowCallback(
    "extension_state",
    extensionId,
    (newState: { counter: number; last_updated: string }) => newState,
    [],
  );

  const handleIncrement = () => {
    setExtensionState({
      counter: (extensionState?.counter ?? 0) + 1,
      last_updated: new Date().toISOString(),
    });
  };

  return <Button onClick={handleIncrement}>Increment</Button>;
}
```

# Opening Tabs

Extensions can open new tabs in the main application using the `useTabs` hook:

```tsx
import { useTabs } from "@hypr/tabs";

function MyExtension() {
  const openNew = useTabs((state) => state.openNew);

  const handleOpenSession = (sessionId: string) => {
    openNew({ type: "sessions", id: sessionId });
  };

  const handleOpenCalendar = () => {
    openNew({ type: "calendars", month: new Date() });
  };

  return (
    <div>
      <Button onClick={() => handleOpenSession("session-123")}>
        Open Session
      </Button>
      <Button onClick={handleOpenCalendar}>Open Calendar</Button>
    </div>
  );
}
```

# Building Extensions

Extensions are built using the build script in the `extensions/` directory. The build process compiles TypeScript/TSX files into browser-compatible JavaScript bundles.

## Commands

```bash
# Build all extensions
pnpm -F @hypr/extensions build

# Build a specific extension
pnpm -F @hypr/extensions build:hello-world

# Or using the build script directly
node build.mjs build              # Build all
node build.mjs build hello-world  # Build specific extension
node build.mjs clean              # Remove all dist folders
node build.mjs install            # Install to app data directory
```

## Build Output

The build process generates:

- `dist/ui.js` - Bundled panel UI (IIFE format)
- `dist/ui.js.map` - Source map for debugging

# Development Workflow

Follow these steps to develop and test an extension:

## 1. Create Extension Directory

```bash
mkdir extensions/my-extension
cd extensions/my-extension
```

## 2. Create Manifest

Create `extension.json` with your extension configuration.

## 3. Create Runtime Script

Create `main.js` with lifecycle handlers.

## 4. Create Panel UI

Create `ui.tsx` with your React component.

## 5. Build

```bash
pnpm -F @hypr/extensions build my-extension
```

## 6. Install for Development

Copy the extension to the app data directory:

```bash
# Using the build script
pnpm -F @hypr/extensions install:dev

# Or manually (macOS)
cp -r extensions/my-extension ~/Library/Application\ Support/com.hyprnote.dev/extensions/

# Linux
cp -r extensions/my-extension ~/.local/share/com.hyprnote.dev/extensions/

# Windows
cp -r extensions/my-extension %APPDATA%/com.hyprnote.dev/extensions/
```

## 7. Test

Launch Hyprnote in development mode and navigate to your extension panel.

# Security Model

Extension UIs run in sandboxed iframes with restricted capabilities. The security model relies on several trust boundaries and validation checks.

## Iframe Sandbox

Extension iframes use the `sandbox="allow-scripts"` attribute, which restricts the iframe from accessing the parent window's DOM, making top-level navigations, or accessing same-origin storage. Extensions communicate with the parent exclusively through the TinyBase postMessage synchronizer.

## Tauri API Isolation

In iframe contexts, Tauri's `__TAURI_INTERNALS__` is polyfilled with a minimal stub that rejects all invoke calls. This prevents extensions from directly calling Tauri commands. The polyfill runs before any module imports to ensure consistent behavior.

## Script Loading

When an extension panel is loaded, the parent window constructs the script URL using Tauri's `convertFileSrc` API. This converts the local file path from the extension registry into a URL that the iframe can fetch. The `entry_path` originates from the trusted backend extension registry, which only includes extensions that have been explicitly installed by the user.

The iframe host route validates incoming search parameters to ensure `extensionId` and `scriptUrl` are well-formed strings and that `scriptUrl` is a valid URL (rejecting potentially dangerous protocols like `javascript:`). Before executing the fetched script, the host also validates that the response content-type includes `javascript` to avoid executing non-JS payloads.

## State Synchronization Security

The TinyBase synchronizer uses postMessage with origin validation. Messages are only accepted from the expected origin (same as the parent window), and the message format is validated before processing. This prevents malicious iframes from injecting state into the main application.

## Export Validation

After script execution, the host checks that the extension exported a valid React component via `__hypr_panel_exports.default`. Extensions that fail to export a default component will display an error rather than rendering.

# Example: Hello World

The `hello-world` extension in the repository demonstrates a complete extension with:

- Extension manifest with panel definition
- Runtime script with lifecycle handlers
- React UI with local and synchronized state using TinyBase
- Usage of @hypr/ui components

To try it:

```bash
cd extensions
pnpm install
pnpm build:hello-world
pnpm install:dev
```

Then launch Hyprnote with `ONBOARDING=0 pnpm -F desktop tauri dev` and click on the profile area (shows "Unknown" by default) to expand the menu, then click "Hello World" to see the extension panel.

# Example: Calendar

The `calendar` extension demonstrates a more complex extension that:

- Reads calendar and event data from the main app's store
- Uses the `@hypr/tabs` API to open sessions
- Implements a full calendar UI with month navigation

To try it:

```bash
cd extensions
pnpm install
pnpm build:calendar
pnpm install:dev
```
